import java.util.*;

public class Simulator {
	private ArrayList<CPU> m_cores;
	private ArrayList<Process> m_readyQueue;
	private ArrayList<Process> m_delayedToReady;
	private ArrayList<Process> m_waitingQueue;
	private ArrayList<Process> m_runningList;
	private ArrayList<ProcessArrival> m_arrivals;
	private ProcessControlBlockTable m_pcbs;
	private ScheduleManager m_scheduler;
	private int m_ticks;

	// Constructors
	//
	public Simulator(ArrayList<ProcessArrival> arrivals) {
		this(arrivals, 1);
	}

	public Simulator(ArrayList<ProcessArrival> arrivals, int numCores) {
		initCores(numCores);
		init();
		m_arrivals = arrivals;
	}
	
	public Simulator(ArrayList<ProcessArrival> arrivals, int numCores, IScheduler scheduler){
		this(arrivals, numCores);
		m_scheduler.setScheduler(scheduler);
	}

	private void init() {
		m_ticks = 0;
		m_pcbs = new ProcessControlBlockTable();
		m_readyQueue = new ArrayList<Process>();
		m_waitingQueue = new ArrayList<Process>();
		m_runningList = new ArrayList<Process>();
		m_delayedToReady = new ArrayList<Process>();
		m_scheduler = new ScheduleManager();
	}

	private void initCores(int numCores) {
		m_cores = new ArrayList<CPU>();
		for (int i = 0; i < numCores; ++i) {
			m_cores.add(new CPU());
		}
	}
	
	// Accessors
	//
	public int getSimulationCycleCount() {
		return m_ticks;
	}
	
	public ArrayList<CPU> getCPUs() {
		return m_cores;
	}
	
	public ProcessControlBlockTable getProcessTable(){
		return m_pcbs;
	}
	
	public ScheduleManager getScheduler() {
		return m_scheduler;
	}

	// Mutators
	//
	public void setScheduler(IScheduler scheduler){
		m_scheduler.setScheduler(scheduler);
	}
	
	// Run simulation
	//
	public void run() {
		// Make sure a scheduler is assigned and initialized with the
		// waiting queue.
		//
		if (m_scheduler == null || m_scheduler.getScheduler() == null) {
			System.out.println("Simulator cannot run without a scheduler.");
			return;
		}

		AbstractScheduler sched = (AbstractScheduler) m_scheduler.getScheduler();
		sched.setQueue(m_readyQueue);
		
		//
		// Start of simulation
		//

		m_ticks = 0; // number of simulation cycles

		while (!m_arrivals.isEmpty() || !m_readyQueue.isEmpty()
				|| !m_waitingQueue.isEmpty() || !m_runningList.isEmpty() 
				|| !m_delayedToReady.isEmpty()) {
			// Check for process arrivals and process them in
			//
			handleArrivals();
			
			// Delayed move of previous processes into ready queue
			//
			for (Process process : m_delayedToReady) {
				addReadyQueue(process);
			}
			m_delayedToReady.clear();

			
			// Look for idle CPUs to assign a process to
			//
			assignIdleCPUs();

			// Run all the cores
			//
			for (CPU core : m_cores) {
				core.execute();
			}			
			
			// Updates PCBs with numbers to later crunch for statistics
			//
			this.m_pcbs.updateAllPCBs();

			// Remove an I/O cycle from all the processes in waiting queue
			//
			simulateWaitCycle();
			
			// Post execution running process handling
			//
			postExecuteCPUCleanup();

			++m_ticks;
		}
	}
	
	// Add process to ready queue
	//
	private void addReadyQueue(Process process)
	{
		m_readyQueue.add(process);
		
		ProcessControlBlock pcb = m_pcbs.find(process); 
		pcb.setState(State.READY);
	}
	
	// Add process to waiting queue
	//
	private void addWaitingQueue(Process process)
	{
		m_waitingQueue.add(process);
		
		ProcessControlBlock pcb = m_pcbs.find(process); 
		pcb.setState(State.WAITING);
	}
	
	// Add process to running list
	//
	private void addRunningList(Process process)
	{
		m_runningList.add(process);
		
		ProcessControlBlock pcb = m_pcbs.find(process); 
		pcb.setState(State.RUNNING);
	}

	// Look at the process arrival list and grab the ones that
	// arrive at the specified time.
	//
	private void handleArrivals() {
		ArrayList<ProcessArrival> removed = new ArrayList<ProcessArrival>();
		for (ProcessArrival arrival : m_arrivals) {
			//
			// Processes that have arrived at the specified time will be removed
			// from the arrival list and added to the PCB table and ready queue.
			if (arrival.getArrival() == m_ticks) {
				removed.add(arrival);
				m_pcbs.add(arrival.getProcess());
				addReadyQueue(arrival.getProcess());
			}
		}
		//
		// Remove processes that are now in ready queue
		for (ProcessArrival arrival : removed) {
			m_arrivals.remove(arrival);
		}
	}
	//
	// Assign a process to CPUs that are idle
	private void assignIdleCPUs() {
		IScheduler sched = m_scheduler.getScheduler();

		for (CPU core : m_cores) {
			if (core.getProcess() == null && m_readyQueue.size() > 0) {
				Process readyProcess = null;

				ScheduleProcess result = sched.getProcess();
				long quantum = result.Quantum;
				readyProcess = result.Process;

				m_readyQueue.remove(readyProcess);
				addRunningList(readyProcess);
				core.setProcess(readyProcess);

				// Set the quantum in the PCB
				//
				ProcessControlBlock pcb = m_pcbs.find(readyProcess);
				pcb.setQuantum(quantum);
			}
		}
	}

	// Simulate a "wait" for all processes in waiting queue
	//
	private void simulateWaitCycle() {
		ArrayList<Process> removed = new ArrayList<Process>();
		for (Process process : m_waitingQueue) {
			ICycle cycle = process.peekNextCycle();
			process.consumeCycle();

			// If the next cycle is a CPU cycle, then lets move
			// the process to the ready queue
			//
			cycle = process.peekNextCycle();
			if (cycle != null && cycle instanceof CPUCycle) {
				removed.add(process);
				addReadyQueue(process);
			} else if (cycle == null) {
				//System.out.println("ERROR: Process's last cycle was an IO cycle.");
				m_pcbs.find(process).setState(State.TERMINATED);
				removed.add(process);
			}
		}

		for (Process process : removed) {
			m_waitingQueue.remove(process);
		}
	}

	// Process the running list after it executed
	//
	private void postExecuteCPUCleanup() {
		ArrayList<Process> removedRunning = new ArrayList<Process>();

		// Decrement quantum for each running process
		//
		for (Process process : m_runningList) {
			ProcessControlBlock pcb = m_pcbs.find(process);
			long quantum = pcb.getQuantum();
			pcb.setQuantum(--quantum);

			if (process.isDone()) {
				// Process is finished so remove from running list
				// and make CPU available.
				//
				m_pcbs.find(process).setState(State.TERMINATED);
				removedRunning.add(process);

				CPU cpu = getCPUByProcess(process);
				cpu.setProcess(null);
			} else {
				// Process still has more to go
				//
				ICycle cycle = process.peekNextCycle();
				if (cycle != null && cycle instanceof IOCycle) {
					// Waiting is next so move to waiting queue; even
					// if time quantum is also expired.
					//
					removedRunning.add(process);
					addWaitingQueue(process);

					CPU cpu = getCPUByProcess(process);
					cpu.setProcess(null);
				} else {
					if (pcb.isQuantumExpired()) {
						// Put it back in the ready queue and clear the CPU for
						// a new process.
						//
						removedRunning.add(process);
						m_delayedToReady.add(process);

						CPU cpu = getCPUByProcess(process);
						cpu.setProcess(null);
					}
				}
			}
		}

		for (Process process : removedRunning) {
			m_runningList.remove(process);
		}
	}

	// Return the CPU object that has a given process
	//
	private CPU getCPUByProcess(Process process) {
		CPU cpu = null;
		for (CPU tcpu : m_cores) {
			if (tcpu.getProcess() == process) {
				cpu = tcpu;
				break;
			}
		}
		return cpu;
	}
}
